package supernova

import (
	"database/sql"
	"errors"
	"fmt"
	"net"
	"net/url"
	"strings"
	"time"

	"gddgame.cc/galaxy/utils/orm"
)

type EngineType string

const (
	Mysql EngineType = "mysql"
)

type OperationType string

type databaseEngine interface {
	GetTableNames(prefix string) ([]string, error)
	GetTableSchema(name string) (*Table, error)
	init(engine *Engine)
	Start(preference *Preference) error
	Finish() error
	Cancel() error
	QueryOperation(operation Operation) (interface{}, error)
	ExecOperation(operation Operation) error
}

type Preference struct {
	define map[string]string

	def *Preference
}

func NewPreference() *Preference {
	preference := &Preference{}
	preference.define = map[string]string{}
	return preference
}

func (preference *Preference) Set(key string, value string) {
	preference.define[key] = value
}

func (preference *Preference) Get(key string) string {
	if v, ok := preference.define[key]; ok {
		return v
	}
	if preference.def != nil {
		return preference.def.Get(key)
	}
	return ""
}

func (preference *Preference) Merge(parent *Preference) *Preference {
	parent.def = preference
	return parent
}

type Operation struct {
	Type   OperationType
	Params []interface{}
}

type Engine struct {
	databaseEngine
	dsn      string
	executor orm.Executor
	*Preference
}

func NewEngineWithDsn(dsn string) (*Engine, error) {
	executor, u, err := connectDatabase(dsn)
	if err != nil {
		return nil, err
	}
	engine := NewEngineWithExecutor(executor, EngineType(u.Scheme))
	return engine, nil
}

func NewEngineWithExecutor(executor orm.Executor, schema EngineType) *Engine {
	engine := &Engine{
		executor:   executor,
		Preference: NewPreference(),
	}
	switch schema {
	case Mysql:
		engine.databaseEngine = &mysqlLogic{}
	}
	if engine.databaseEngine != nil {
		engine.init(engine)
	}

	return engine
}

func (engine *Engine) Close() {
	_ = engine.executor.Close()
}

func (engine *Engine) AnalysisDatabase(prefix string) ([]*Table, error) {
	if engine.databaseEngine == nil {
		return nil, errors.New("database need dsn or orm.Executor")
	}
	tableNames, err := engine.GetTableNames(prefix)
	if err != nil {
		return nil, err
	}
	if tableNames == nil || len(tableNames) == 0 {
		return []*Table{}, nil
	}
	tables := make([]*Table, len(tableNames))
	for index, tableName := range tableNames {
		t, err := engine.GetTableSchema(tableName)
		if err != nil {
			return nil, err
		}
		tables[index] = t
	}
	return tables, nil
}

func (engine *Engine) QueryDatabase(operations []Operation, preference *Preference) ([]interface{}, error) {
	if engine.databaseEngine == nil {
		return nil, errors.New("database need dsn or orm.Executor")
	}
	if preference == nil {
		preference = engine.Preference
	}
	result := make([]interface{}, len(operations))
	for index, operation := range operations {
		rrr, err := engine.databaseEngine.QueryOperation(operation)
		if err != nil {
			return nil, err
		}
		result[index] = rrr
	}
	return result, nil
}

func (engine *Engine) ExecDatabase(operations []Operation, preference *Preference) error {
	if engine.databaseEngine == nil {
		return errors.New("database need dsn or orm.Executor")
	}
	if preference == nil {
		preference = engine.Preference
	}
	if err := engine.databaseEngine.Start(preference); err != nil {
		return err
	}
	var err error
	for _, operation := range operations {
		err = engine.databaseEngine.ExecOperation(operation)
		if err != nil {
			break
		}
	}
	if err != nil {
		if err := engine.databaseEngine.Cancel(); err != nil {
			return err
		}
	} else if err := engine.databaseEngine.Finish(); err != nil {
		return err
	}

	return err
}

func (engine *Engine) Orm(model *orm.Model) *Table {
	return nil
}

func NeedCreate(err error) bool {
	if strings.Contains(err.Error(), "Unknown database") {
		// mysql error: database
		return true
	}
	return false
}

func connectDatabase(dsn string) (orm.Executor, *url.URL, error) {
	u, err := url.Parse(dsn)
	if err != nil {
		return nil, nil, err
	}
	host, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		host = u.Host
	}
	if host == "" {
		return nil, u, fmt.Errorf("%s host is empty: %s", u.Scheme, u)
	}
	if port == "" {
		port = "3306"
	}
	db, err := sql.Open(u.Scheme, fmt.Sprintf("%s@tcp(%s)%s?charset=utf8", u.User, u.Host, u.Path))
	if err != nil {
		return nil, u, err
	}
	db.SetConnMaxLifetime(time.Minute)

	executor := orm.NewExecutor()
	executor.ConnWithDriver(db, u.Scheme)

	return executor, u, nil
}
